/*
 *  This file is part of Bracket Properties
 *  Copyright 2011-2013 David R. Smith
 *  All Rights Reserved
 *
 */
package asia.redact.bracket.properties.serializer;

import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.log4j.Logger;

import asia.redact.bracket.properties.Properties;

/**
 *<p> Walks a java object graph. The Renderer encapsulates the actions to be carried out as we walk over the graph.
 * Here's an example:</p>
 * 
 * <pre>
		HashMap<String,Date> map = new HashMap<String,Date>();
		map.put("mykey1", new Date());
		map.put("mykey2", new Date());
		Graph g = new Graph(new PropertiesRenderer(), map);
		g.walk();
		Properties props = g.getProperties();
		OutputAdapter out = new OutputAdapter(props);
		try {
			StringWriter writer = new StringWriter();
			out.writeTo(writer);
			System.out.println(writer.toString());
		} catch (IOException e) {
			e.printStackTrace();
		}
 * 
 * </pre>
 * 
 * <p>Will output</p>
 * 
 * <pre>
#;; content=text/x-java-properties
#;; charset=UTF-8
#;; last-generated-on=Sun Jun 02 18:45:26 EST 2013
asia.redact.bracket.properties.serializer.INITIAL_KEY=0
0=MAP|java.util.HashMap
0.0.k=mykey1
0.0.v=$1
1=OBJECT|java.util.Date
1.time=1370162726779
0.1.k=mykey2
0.1.v=$2
2=OBJECT|java.util.Date
2.time=1370162726779
asia.redact.bracket.properties.serializer.LAST_KEY=3

#;; end properties file 
 * </pre>
 * 
 * <p>This class has a number of important limitations (constraints) on the types of classes it can serialize:</p>
 * 
 * <ol>
 *  <li>The classes must have a default constructor</li>
 *  <li>The classes must have accessor methods for every property that needs to be serialized</li>
 *  <li>The accessor methods must have only one parameter, for example setItem(Item) not setDimension(width,height)</li>
 *  <li>The classes must not have internal cycles which would create a loop. Currently there is no loop detection</li>
 *  <li>Classes with complex serialization requirements can use the optional "Loadable" Interface instead of setters.</li>
 * </ol>
 * 
 * @author Dave
 *
 */
public class Graph {

	private static int MAX_DEPTH = 100000;
	
	Logger log = Logger.getLogger(Graph.class);
	final Object root;
	final Renderer renderer;
	int depth;
	
	public Graph(Object root) {
		super();
		this.root = root;
		renderer = new PropertiesRenderer();
	}
	
	public Graph(Renderer renderer, Object root) {
		super();
		this.root = root;
		this.renderer = renderer;
	}

	/**
	 * Top level recursion - walk the object graph of the root object based on its accessors. Returns the found properties
	 * or null if the Renderer does not support generating Properties (the Renderer might just do logging or analysis).
	 * 
	 */
	public Properties walk() {
		TokenFactory.INSTANCE.resetSerialID();
		TokenType tt = MethodGetter.testType(root);
		long start = TokenFactory.INSTANCE.currentSerialID()-1;
		Token rootToken = TokenFactory.INSTANCE.createToken(start,tt, "", root);
		if(root != null && root.getClass().isArray()){
			long length = Array.getLength(root);
			rootToken.setArrayLength(length);
		}
		renderer.renderInitialKeyId(rootToken);
		walk(rootToken);
		if(TokenFactory.INSTANCE.currentSerialID()>start) {
			renderer.renderLastKeyId(TokenFactory.INSTANCE.currentSerialID());
		}else{
			renderer.renderLastKeyId(start+1);
		}
		
		renderer.renderClassMap();
		
		return this.getProperties();
	}
	
	/**
	 * does a recursive call to walk the object graph
	 * 
	 * @param parentToken
	 */
	@SuppressWarnings("rawtypes")
	private void walk(Token parentToken) {
		
		if(depth > MAX_DEPTH) {
			log.error("Depth has hit MAX_DEPTH, exiting walk()");
			return;
		}
		depth++;

		
		switch(parentToken.getTokenType()){
			case N:  break;
			case LOADABLE: {
				renderer.renderParentClass(parentToken);
				renderer.renderLoadable(parentToken);
				break;
			}
			case P: {
				renderer.renderValue(parentToken);
				break;
			}
			case S: {
				renderer.renderValue(parentToken);
				break;
			}
			case I: {
				renderer.renderValue(parentToken);
				break;
			}
			case LO: {
				renderer.renderValue(parentToken);
				break;
			}
			case D: {
				renderer.renderValue(parentToken);
				break;
			}
			case F: {
				renderer.renderValue(parentToken);
				break;
			}
			case B: {
				renderer.renderValue(parentToken);
				break;
			}
			
			case O: {
				renderer.renderParentClass(parentToken);
				MethodGetter getter = new MethodGetter(parentToken);
				List<Method> gMethods = getter.listGetters();
				for(Method m: gMethods) {
					Token token = getter.invoke(m);
					if(token.getId()==0)continue;
					if(token.isRenderImmediate()){
						renderer.renderImmediate(parentToken, token);
					}else{
						renderer.renderIndirect(parentToken, token);
					    walk(token);
					}
				}
				break;
			}
			case LI: {
				renderer.renderParentClass(parentToken);
				List<?> list = (List<?>) parentToken.getObject();
				int count = 0;
				for(Object obj : list){
					//set list token
					Token listItemToken = TokenFactory.INSTANCE.createTmpToken(obj);
					if(listItemToken.isRenderImmediate()){
						renderer.renderImmediateListItem(parentToken, listItemToken, count);
					}else{
						renderer.renderIndirectListItem(parentToken, listItemToken, count);
						renderer.renderParentClass(listItemToken);
						MethodGetter getter = new MethodGetter(listItemToken);
						List<Method> gMethods = getter.listGetters();
						for(Method m: gMethods) {
							Token token = getter.invoke(m);
							if(token.getId()==0)continue;
							if(token.isRenderImmediate()){
								renderer.renderImmediate(listItemToken, token);
							}else{
								renderer.renderIndirect(listItemToken, token);
								walk(token);
							}
						}
					}
					count++;
				}
				break;
			}
			
			case A: {
				renderer.renderParentClass(parentToken);
				Object o = parentToken.getObject();
				
				// primitives - TODO, add double, etc.
				if(o instanceof char[]){
					renderer.renderCharArray(parentToken);
					break;
				}else if(o instanceof int[]){
					renderer.renderIntArray(parentToken);
					break;
				}if(o instanceof byte[]){
					renderer.renderByteArray(parentToken);
					break;
				}if(o instanceof long[]){
					renderer.renderLongArray(parentToken);
					break;
				}if(o instanceof float[]){
					renderer.renderFloatArray(parentToken);
					break;
				}if(o instanceof double[]){
					renderer.renderDoubleArray(parentToken);
					break;
				}if(o instanceof boolean[]){
					renderer.renderBooleanArray(parentToken);
					break;
				}
				
				// handle Object arrays
				Object[] list = (Object[]) parentToken.getObject();
				int count = 0;
				for(Object obj : list){
					//set list token
					Token listItemToken = TokenFactory.INSTANCE.createTmpToken(obj);
					if(listItemToken.isRenderImmediate()){
						renderer.renderImmediateListItem(parentToken, listItemToken, count);
					}else{
						renderer.renderIndirectListItem(parentToken, listItemToken, count);
						renderer.renderParentClass(listItemToken);
						MethodGetter getter = new MethodGetter(listItemToken);
						List<Method> gMethods = getter.listGetters();
						for(Method m: gMethods) {
							Token token = getter.invoke(m);
							if(token.getId()==0)continue;
							if(token.isRenderImmediate()){
								renderer.renderImmediate(listItemToken, token);
							}else{
								renderer.renderIndirect(listItemToken, token);
								walk(token);
							}
						}
					}
					count++;
				}
				break;
			}
			
			case E: {
				renderer.renderParentClass(parentToken);
				MethodGetter getter = new MethodGetter(parentToken);
				Method m = MethodGetter.getEnumNameMethod();
				Token token = getter.invoke(m);
				renderer.renderImmediate(parentToken, token);
				break;
			}
			case M: {
				renderer.renderParentClass(parentToken);
				int internalCount = 0;
				Map map = (Map) parentToken.getObject();
				Set set = map.keySet();
				for(Object key: set){
					
					//set key token
					Token keyToken = TokenFactory.INSTANCE.createTmpToken(key);
					if(keyToken.isRenderImmediate()){
						renderer.renderImmediateMapKey(parentToken, keyToken, internalCount);
					}else{
						renderer.renderIndirectMapKey(parentToken, keyToken, internalCount);
						renderer.renderParentClass(keyToken);
						MethodGetter getter = new MethodGetter(keyToken);
						List<Method> gMethods = getter.listGetters();
						for(Method m: gMethods) {
							Token token = getter.invoke(m);
							if(token.getId()==0)continue;
							if(token.isRenderImmediate()){
								renderer.renderImmediate(keyToken, token);
							}else{
								renderer.renderIndirect(keyToken, token);
								walk(token);
							}
						}
					}
					
					// set value
					Object value = map.get(key);
					Token valueToken = TokenFactory.INSTANCE.createTmpToken(value);
					if(valueToken.isRenderImmediate()){
						renderer.renderImmediateMapValue(parentToken, valueToken, internalCount);
					}else{
						renderer.renderIndirectMapValue(parentToken, valueToken, internalCount);
						renderer.renderParentClass(valueToken);
						MethodGetter getter = new MethodGetter(valueToken);
						List<Method> gMethods = getter.listGetters();
						for(Method m: gMethods) {
							Token token = getter.invoke(m);
							if(token.getId()==0)continue;
							if(token.isRenderImmediate()){
								renderer.renderImmediate(valueToken, token);
							}else{
								renderer.renderIndirect(valueToken, token);
								walk(token);
							}
						}
					}
					internalCount++;
				}
				break;
			}
			
			default: break;
		}
		
		depth--;
		return;
	}

	public Object getRoot() {
		return root;
	}

	public Renderer getRenderer() {
		return renderer;
	}
	
	public Properties getProperties() {
		if(renderer instanceof PropertiesRenderer){
			return ((PropertiesRenderer)renderer).getProps();
		}else{
			if(renderer instanceof Wrapper){
				return((Wrapper)renderer).getProperties();
			}
		}
		
		return null;
	}

}
